﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Xml.Linq;
using System.Xml;
using System.IO;
using LockCrypt.Core;
using Newtonsoft.Json;

namespace LockCrypt.DAL {
    /// <summary>
    /// Provides access to legacy LockCrypt databases.
    /// </summary>
    public class LegacyProvider : FileBasedDatabase, IDbProvider {
        private readonly LegacyEncryptionService _encryptionService;
        private bool? _isConnected;
        
        /// <summary>
        /// Gets the legacy encryption provider.
        /// </summary>
        /// <value>The legacy encryption provider.</value>
        public IEncryptionService Encryption {
            get { return _encryptionService; }
        }

        /// <summary>
        /// Gets the encryption key.
        /// </summary>
        /// <value>The encryption key.</value>
        public string EncryptionKey {
            get { return _encryptionService.EncryptionKey; }
            set { _encryptionService.EncryptionKey = value; }
        }

        /// <summary>
        /// Gets the type of database which this provider can access.
        /// </summary>
        /// <value>The type of database which this provider can access.</value>
        public Constants.DatabaseType DatabaseType {
            get { return Constants.DatabaseType.Legacy; }
        }

        /// <summary>
        /// Gets a value indicating whether this instance is either connected or can connect.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance is connected or can connect, otherwise <c>false</c>.
        /// </value>
        public bool IsConnected {
            get {
                if(!_isConnected.HasValue) {
                    _isConnected = CheckPassword();
                }
                return _isConnected.Value;
            }
        }

        /// <summary>
        /// Initializes a new <see cref="LegacyProvider"/> for a specified file, using the current encryption key.
        /// </summary>
        /// <param name="fileName">Name of the database file.</param>
        public LegacyProvider(string fileName) 
            :base(fileName) {
            byte[] iv = new byte[16];
            if(File.Exists(fileName)) { // read existing IV
                using(FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read)) {
                    if(fs.Length <= 16)
                        throw new LockCryptException(I18nUtils.GetString("Errors", "InvalidDatabase") + " (#4b)");
                    try {
                        fs.Read(iv, 0, 16);
                        Console.WriteLine("Read IV: " + iv.Aggregate("", (items, item) => items + "-" + item.ToString()));
                    } catch {
                        throw new LockCryptException(I18nUtils.GetString("Errors", "ErrorReadingIV") + " (#4c)");
                    }
                }
            } else { // new file, pick an IV
                RNGCryptoServiceProvider.Create().GetBytes(iv);
                Console.WriteLine("Created IV: " + iv.Aggregate("", (items, item) => items + "-" + item.ToString()));
            }
            _encryptionService = new LegacyEncryptionService(iv);
        }

        #region Accounts
        /// <summary>
        /// Adds accounts to the default database.
        /// </summary>
        /// <param name="accounts">The accounts to add.</param>
        /// <param name="logger">The delegate to use to log progress (can be null).</param>
        public void AddAccounts(IEnumerable<IAccount> accounts, ILogProvider logger) {
            var db = ReadDatabase();
            // if the ID is the same, it's more than likely the same instance, but from an imported file.
            var newIds = db.Accounts.Select(a => a.ID);
            db.Accounts.AddRange(accounts.Where(a => !newIds.Contains(a.ID)));
            SaveDatabase(db, logger);
        }

        /// <summary>
        /// Edits an account.
        /// </summary>
        /// <param name="account">The account.</param>
        public void EditAccount(IAccount account) {
            var db = ReadDatabase();
            db.Accounts.RemoveAll(a => a.ID == account.ID);
            db.Accounts.Add(account);
            SaveDatabase(db, null);
        }

        /// <summary>
        /// Deletes an account.
        /// </summary>
        /// <param name="accountId">The account ID.</param>
        public void DeleteAccount(string accountId) {
            var db = ReadDatabase();
            db.Accounts.RemoveAll(a => a.ID == accountId);
            SaveDatabase(db, null);
        }

        public List<IAccount> GetAccounts() {
            List<IAccount> accounts = new List<IAccount>();
            ILogProvider logger = new ConsoleLogger();
            using(CryptoStream cStream = GetDatabaseStream())
            using(StreamReader reader = new StreamReader(cStream)) {
                string line;
                JsonSerializer jsonReader = new JsonSerializer();
                while((line = reader.ReadLine()) != null) {
                    if(line.Length < 5)
                        continue;
                    try {
                        if(line.Substring(0, 3) == "acc") {
                            accounts.Add(JSONConverters.JSONToAccount(jsonReader, line.Substring(3)));
                        }
                    } catch(Exception ex) {
                        logger.Log("Invalid account: " + ex.Message);
                    }
                }
            }
            return accounts;
        }
        #endregion

        #region Groups
        /// <summary>
        /// Adds account groups to the default database.
        /// </summary>
        /// <param name="groups">The groups to add.</param>
        /// <param name="logger">The delegate to use to log progress (can be null).</param>
        public void AddGroups(IEnumerable<IGroup> groups, ILogProvider logger) {
            var db = ReadDatabase();
            // if the ID is the same, it's more than likely the same instance, but from an imported file.
            var newGroupIds = db.Groups.Select(g=>g.ID);
            db.Groups.AddRange(groups.Where(g=> !newGroupIds.Contains(g.ID)));
            SaveDatabase(db, logger);
        }

        /// <summary>
        /// Edits a group.
        /// </summary>
        /// <param name="group">The group.</param>
        public void EditGroup(IGroup group) {
            var db = ReadDatabase();
            db.Groups.RemoveAll(g => g.ID == group.ID);
            db.Groups.Add(group);
            SaveDatabase(db, null);
        }

        /// <summary>
        /// Deletes a group and all child groups/accounts.
        /// </summary>
        /// <param name="group">The group.</param>
        public void DeleteGroup(IGroup group) {
            var db = ReadDatabase();
            IEnumerable<INestable> nestedGroups = LockCryptBLL.ArrangeGroups(db.Groups, null);
            IGroup groupToDelete = null; // find the group in the heirarchy
            foreach(var nestedGroup in nestedGroups) {
                // search top level groups
                groupToDelete = group.FindGroup(group.ID);
                if(groupToDelete != null)
                    break;
            }
            IEnumerable<IGroup> unavailableGroups = (groupToDelete).Flatten();
            var groupIds = unavailableGroups.Select(g => g.ID);

            db.Groups.RemoveAll(g => groupIds.Contains(g.ID));
            SaveDatabase(db, null);
        }

        /// <summary>
        /// Gets a list of account groups from the current database.
        /// </summary>
        /// <returns>A list of account groups in the database.</returns>
        public List<IGroup> GetGroups() {
            List<IGroup> groups = new List<IGroup>();
            ILogProvider logger = new ConsoleLogger();
            using(CryptoStream cStream = GetDatabaseStream())
            using(StreamReader reader = new StreamReader(cStream)) {
                string line;
                JsonSerializer jsonReader = new JsonSerializer();
                while((line = reader.ReadLine()) != null) {
                    if(line.Length < 5)
                        continue;
                    try {
                        if(line.Substring(0, 3) == "grp") {
                            groups.Add(JSONConverters.JSONToGroup(jsonReader, line.Substring(3)));
                        }
                    } catch(Exception ex) {
                        logger.Log("Invalid group: " + ex.Message);
                    }
                }
            }
            return groups;
        }
        #endregion

        #region Templates
        /// <summary>
        /// Adds templates to the default database.
        /// </summary>
        /// <param name="templates">The templates to add.</param>
        /// <param name="logger">The delegate to use to log progress (can be null).</param>
        /// <returns><c>null</c> on success, a log of errors on failure.</returns>
        public void AddTemplates(IEnumerable<ITemplate> templates, ILogProvider logger) {
            var db = ReadDatabase();
            // if the ID is the same, it's more than likely the same instance, but from an imported file.
            var newIds = db.Templates.Select(t => t.ID);
            db.Templates.AddRange(templates.Where(t => !newIds.Contains(t.ID)));
            SaveDatabase(db, logger);
        }

        /// <summary>
        /// Edits a template.
        /// </summary>
        /// <param name="template">The template.</param>
        public void EditTemplate(ITemplate template) {
            var db = ReadDatabase();
            db.Templates.RemoveAll(t => t.ID == template.ID);
            db.Templates.Add(template);
            SaveDatabase(db, null);
        }

        /// <summary>
        /// Deletes a template.
        /// </summary>
        /// <param name="templateId">The template ID.</param>
        public void DeleteTemplate(string templateId) {
            var db = ReadDatabase();
            db.Templates.RemoveAll(t => t.ID == templateId);
            SaveDatabase(db, null);
        }

        /// <summary>
        /// Gets a list of templates from the current database file.
        /// </summary>
        /// <returns>A list of templates in the database.</returns>
        public List<ITemplate> GetTemplates() {
            List<ITemplate> templates = new List<ITemplate>();
            ILogProvider logger = new ConsoleLogger();
            using(CryptoStream cStream = GetDatabaseStream())
            using(StreamReader reader = new StreamReader(cStream)) {
                string line;
                JsonSerializer jsonReader = new JsonSerializer();
                while((line = reader.ReadLine()) != null) {
                    if(line.Length < 5)
                        continue;
                    try {
                        if(line.Substring(0, 3) == "typ") {
                            templates.Add(JSONConverters.JSONToTemplate(jsonReader, line.Substring(3)));
                        }
                    } catch(Exception ex) {
                        logger.Log("Invalid template: " + ex.Message);
                    }
                }
            }
            return templates;
        }

        /// <summary>
        /// Gets matching templates by name.
        /// </summary>
        /// <param name="templateName">Name of the template.</param>
        /// <returns>All templates with a matching name.</returns>
        public List<ITemplate> GetTemplatesByName(string templateName) {
            return GetTemplates().Where(t => t.Name.Equals(templateName, StringComparison.InvariantCultureIgnoreCase)).ToList();
        }
        #endregion

        #region Database Maintenance
        /// <summary>
        /// Checks the password entered is valid.
        /// </summary>
        /// <returns>
        /// 	<c>true</c> if the password is valid, otherwise <c>false</c>.
        /// </returns>
        public bool CheckPassword() {
            bool valid = false;
            try {
                using(CryptoStream cStream = GetDatabaseStream())
                using(StreamReader reader = new StreamReader(cStream)) {
                    string check = reader.ReadLine();
                    if(check.ToLowerInvariant().EndsWith("lockcrypt")) {
                        valid = true;
                        _isConnected = true;
                    }
                }
            } catch {}
            return valid;
        }

        /// <summary>
        /// Gets a decrypted stream from the database.
        /// </summary>
        /// <returns>A decrypted stream for the file.</returns>
        private CryptoStream GetDatabaseStream() {
            FileStream fs = File.OpenRead(_filename);
            fs.Seek(16, SeekOrigin.Begin);
            return _encryptionService.Decrypt(fs);
        }

        /// <summary>
        /// Creates a new database file.
        /// </summary>
        /// <exception cref="System.Exception">Multiple exceptions, check the reference for DirectoryInfo.Create and the FileInfo constructor.</exception>
        public void CreateDatabase() {
            FileInfo info = new FileInfo(_filename);
            if(!info.Directory.Exists) {
                info.Directory.Create();
            }
            SaveDatabase(new DatabaseContents(), null);
        }

        /// <summary>
        /// Clears the database.
        /// </summary>
        public void ClearDatabase() {
            SaveDatabase(new DatabaseContents(), null);
        }

        /// <summary>
        /// Reads the whole contents of the database.
        /// </summary>
        /// <returns>The whole contents of the database.</returns>
        public DatabaseContents ReadDatabase() {
            List<IAccount> accounts = new List<IAccount>();
            List<IGroup> groups = new List<IGroup>();
            List<ITemplate> templates = new List<ITemplate>();
            ILogProvider logger = new ConsoleLogger();
            using(CryptoStream cStream = GetDatabaseStream())
            using(StreamReader reader = new StreamReader(cStream)) {
                string line;
                JsonSerializer jsonReader = new JsonSerializer();
                while((line = reader.ReadLine()) != null) {
                    if(line.Length < 5)
                        continue;
                    try {
                        switch (line.Substring(0, 3)) {
                            case "grp":
                                groups.Add(JSONConverters.JSONToGroup(jsonReader, line.Substring(3)));
                                break;
                            case "acc":
                                IAccount account = JSONConverters.JSONToAccount(jsonReader, line.Substring(3));
                                accounts.Add(account);
                                break;
                            case "typ":
                                templates.Add(JSONConverters.JSONToTemplate(jsonReader, line.Substring(3)));
                                break;
                        }
                    } catch(Exception ex) {
                        logger.Log("Invalid account: " + ex.Message);
                    }
                }
            }
            //todo: templates
            DatabaseContents contents = new DatabaseContents() {
                Accounts = accounts,
                Templates = templates,
                Groups = groups
            };
            //Console.WriteLine(string.Format("{0} accounts, {1} groups, {2} templates", contents.Accounts.Count, contents.Groups.Count, contents.Templates.Count));
            return contents;
        }

        /// <summary>
        /// Saves the contents of the database to a file.
        /// </summary>
        /// <param name="contents">The database contents.</param>
        /// <param name="logger">The logger to use.</param>
        public void SaveDatabase(DatabaseContents contents, ILogProvider logger) {
            if(logger == null) {
                logger = new NullLogger();
            }
            using(FileStream fs = new FileStream(_filename, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)) {
                fs.Write(_encryptionService.InitializationVector, 0, _encryptionService.InitializationVector.Length);
                fs.Flush();
                Console.WriteLine("Wrote IV: " + _encryptionService.InitializationVector.Aggregate("", (items, item) => items + "-" + item.ToString()));

                using(CryptoStream cStream = _encryptionService.Encrypt(fs))
                using(StreamWriter writer = new StreamWriter(cStream)) {
                    writer.WriteLine("LockCrypt");
                    writer.WriteLine(string.Format("LCV{0}WPF", Constants.GetVersion().ToString(3)));
                    foreach(IGroup group in contents.Groups) {
                        string itemString = JsonConvert.SerializeObject(new JSONGroup(group));
                        writer.WriteLine("grp" + itemString);
                    }
                    foreach(IAccount account in contents.Accounts) {
                        var jsoAccount = new JSONAccount(account);
                        IGroup parentGroup = contents.Groups.FirstOrDefault(g => g.ID == account.ParentID);
                        if(parentGroup != null) {
                            jsoAccount.Parent = parentGroup.Name;
                        } else {
                            jsoAccount.Parent = "LockCrypt"; //root account
                        }
                        string itemString = JsonConvert.SerializeObject(jsoAccount);
                        writer.WriteLine("acc" + itemString);
                    }
                    foreach(ITemplate template in contents.Templates) {
                        string itemString = JsonConvert.SerializeObject(new JSONTemplate(template));
                        writer.WriteLine("typ" + itemString);
                    }

                    writer.Write("\n\n");
                    writer.Flush();
                    cStream.FlushFinalBlock();
                }
            }
        }
        #endregion
    }
}
